﻿// Original author contact info: Owen Emlen (owene_1998@yahoo.com)
// Note: other individuals may also have contributed to this code
// Project hosted on CodePlex.com as of 1/10/2009 at http://www.codeplex.com/EmlenMud
using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Threading;
using BrainTechLLC.ThreadSafeObjects;
#if NO_SILVERLIGHT
using BrainTechLLC.Logging;
#endif

namespace BrainTechLLC
{
	/// <summary>
	/// The scheduler executes items of work at the (approximately) appropriately scheduled time
	/// </summary>    
	public class SchedulingEngine : ISchedulingEngine
	{
        public static SchedulingEngine Instance;
		public ThreadSafeQueue<IExecutableWorkItem>[] _allWorkItemQueues;

		private readonly int CheckQueueIntervalMS;
		private readonly int LargeDelayQueueCutoffMS;
		private readonly int MaxExceptionListSize;
		private readonly int ModerateDelayQueueCutoffMS;
		private readonly int QueueIdleSleepTimeMS;

		/// <summary>
		/// Defines the total number of threads to be allocated to this scheduler/engine
		/// In general, more threads are needed as the execution of each work item increases
		/// to avoid long work items blocking other scheduled work
		/// </summary>
		private readonly int TotalThreads;

		private DateTime _currentDateTime;
		private List<Exception> _exceptions;
		private long _executed;
		private WorkItemQueueHandler[] _workItemQueueHandlers;
		private int counter;

		public SchedulingEngine()
		{
			TotalThreads = 5;// Properties.Settings.Default.WorkerThreadCount;
			ModerateDelayQueueCutoffMS = 300;// Properties.Settings.Default.ModerateDelayQueueCutoffMS;
			LargeDelayQueueCutoffMS = 5000;// Properties.Settings.Default.LargeDelayQueueCutoffMS;
			MaxExceptionListSize = 50;// Properties.Settings.Default.MaxExceptionListSize;
			CheckQueueIntervalMS = 36;// Properties.Settings.Default.CheckQueueIntervalMS;
			QueueIdleSleepTimeMS = 18;// Properties.Settings.Default.QueueIdleSleepTimeMS;
			Initialize();
            Instance = this;
		}

		public SchedulingEngine(int workerThreadCount) 
		{
			TotalThreads = workerThreadCount;
			ModerateDelayQueueCutoffMS = 300;// Properties.Settings.Default.ModerateDelayQueueCutoffMS;
			LargeDelayQueueCutoffMS = 5000;// Properties.Settings.Default.LargeDelayQueueCutoffMS;
			MaxExceptionListSize = 50;// Properties.Settings.Default.MaxExceptionListSize;
			CheckQueueIntervalMS = 36;// Properties.Settings.Default.CheckQueueIntervalMS;
			QueueIdleSleepTimeMS = 18;// Properties.Settings.Default.QueueIdleSleepTimeMS;
			Initialize();
            Instance = this;
		}

		[field: NonSerialized]
		public event EventHandler<ObjectEventArgs<Exception>> ExceptionOccurred;

		public DateTime CurrentDateTime
		{
			get { return _currentDateTime; }
			set { _currentDateTime = value; }
		}

		public List<Exception> Exceptions
		{
			get { return _exceptions; }
			set { _exceptions = value; }
		}

		public bool WantExit
		{
			get;
			set;
		}

		public ISupportsCount[] WorkItemQueueCounts
		{
			get { return _allWorkItemQueues.ToInterfaceArray<ISupportsCount, ThreadSafeQueue<IExecutableWorkItem>>(); }
		}

		public long WorkItemsExecuted
		{
			get { return _executed; }
			set { _executed = value; }
		}

		public void HandleWorkItemQueue(int n)
		{
			DateTime lastUpdateQ = DateTime.Now;

			while (!WantExit)
			{
				try
				{
					// Update the current time
					DateTime currentTime = UpdateDateTime();

					// If this is a immediate worker thread (work items scheduled to be executed soon)
					if (n < TotalThreads - 2)
					{
						if (lastUpdateQ.AddMilliseconds(CheckQueueIntervalMS) <= currentTime)
						{
                            // TEST
							lastUpdateQ = currentTime;
							int executedThisPass = ExecuteEventsInQueue(n);

							// If there was nothing to execute, sleep
							if (executedThisPass == 0)
								Thread.Sleep(CheckQueueIntervalMS);
						}
						else
						{
							Thread.Sleep(QueueIdleSleepTimeMS);
						}
					}
					else if (n == TotalThreads - 2)
					{
						// This holds the "medium-term" work items - scheduled to be executed somewhat soon, but not immediately
						if (lastUpdateQ.AddMilliseconds(ModerateDelayQueueCutoffMS - 50) <= currentTime)
						{
							lastUpdateQ = currentTime;
							int updated = UpdateWorkItemSchedule(n);

							if (updated == 0)
								Thread.Sleep(100);
						}
						else
						{
							Thread.Sleep(50);
						}
					}
					else if (n == TotalThreads - 1)
					{
						// This holds the "long-term" work items - scheduled to be executed later
						if (lastUpdateQ.AddMilliseconds(LargeDelayQueueCutoffMS - 100) <= currentTime)
						{
							lastUpdateQ = currentTime;
							int updated = UpdateWorkItemSchedule(n);

							if (updated == 0)
								Thread.Sleep(200);
						}
						else
						{
							Thread.Sleep(100);
						}
					}
				}
				catch (Exception ex)
				{
#if NO_SILVERLIGHT                           
					GlobalLogger.Log(ex, LogSeverity.Critical, true);
                    RuntimeProblemLog.LogProblem(ex.ToString());
#endif
                    if (_exceptions.Count > MaxExceptionListSize)
					{
						try { _exceptions.RemoveRange(4, MaxExceptionListSize - 5); }
						catch { }
					}

					_exceptions.Add(ex);

					if (ExceptionOccurred != null)
						ExceptionOccurred(this, new ObjectEventArgs<Exception>(ex));
					else
						throw;
				}
			}
		}

		/// <summary>
		/// Schedules a work item to execute at a specific time
		/// </summary>
		/// <param name="eventToExecute"></param>
		public void ScheduleEventToExecute(IExecutableWorkItem workItemToExecute)
		{
			int queueNum = FindAppropriateQueue(workItemToExecute);
			_allWorkItemQueues[queueNum].Enqueue(workItemToExecute);
		}

		public void ScheduleEventToExecute(IEnumerable<IExecutableWorkItem> workItemsToExecute)
		{
			foreach (IExecutableWorkItem workItem in workItemsToExecute)
				ScheduleEventToExecute(workItem);
		}

		public void ScheduleEventToExecute(Action action, int milliseconds)
		{
			SimpleWorkItem workItem = new SimpleWorkItem() { Action = action };
			ScheduleEventToExecute(workItem, milliseconds);
		}

		public void ScheduleEventToExecute(IExecutableWorkItem workItemToExecute, int milliseconds)
		{
            if (!WantExit)
            {
                workItemToExecute.ExecutionTime = this._currentDateTime.AddMilliseconds(milliseconds);
                int queueNum = FindAppropriateQueue(workItemToExecute);
                _allWorkItemQueues[queueNum].Enqueue(workItemToExecute);
            }
		}

		/// <summary>
		/// Starts up the scheduling engine
		/// </summary>
		public void StartSchedulingEngine()
		{
			for (int n = 0; n < _allWorkItemQueues.Length; n++)
			{
				_allWorkItemQueues[n] = new ThreadSafeQueue<IExecutableWorkItem>();
				_workItemQueueHandlers[n] = new WorkItemQueueHandler(this, n);
				_workItemQueueHandlers[n].StartEventHandler();
			}
		}
        

		private int ExecuteEventsInQueue(int queueNumber)
		{
			IExecutableWorkItem item;
			DateTime currentTime = CurrentDateTime;

			// Stores executable work items whose execution time has not yet come - and need to be placed back in the queue
			List<IExecutableWorkItem> reschedule = new List<IExecutableWorkItem>();

			// Stores Dequeue'd work items - Dequeues multiple items to reduce repeated calls to Dequeue (and to reduce locking)
			IExecutableWorkItem[] itemsToExecute = new IExecutableWorkItem[16];

			// Keep track of work item executions this pass
			int executedThisPass = 0;

			int dequeued = _allWorkItemQueues[queueNumber].DequeueMultiple(itemsToExecute, 16);

			// Dequeue multiple work items from the queue
			while (dequeued > 0)
			{
				// Check each dequeue'd work item
				for (int n = 0; n < dequeued && !WantExit; n++)
				{
					item = itemsToExecute[n];

					if (item.ExecutionTime > currentTime)
					{
						// Execution time for the work item is still in the future
						reschedule.Add(item);
					}
					else
					{
						// It is time to execute this work item.  Do it.
						executedThisPass++;
						item.Execute();
					}
				}

				dequeued = _allWorkItemQueues[queueNumber].DequeueMultiple(itemsToExecute, 10);
			}

			// Re-queue all work items that were dequeue'd but not executed (not yet their time)
			_allWorkItemQueues[queueNumber].EnqueueMultiple(reschedule);

			// Add to the executed work item total
			if (executedThisPass > 0)
			{
				long execThisPass = Convert.ToInt64(executedThisPass);
				Interlocked.Add(ref _executed, execThisPass);
			}

			return executedThisPass;
		}

		/// <summary>
		/// Determines the appropriate queue in which to place the work item, based on its
		/// scheduled execution time
		/// </summary>
		/// <param name="eventToExecute"></param>
		/// <returns></returns>
		private int FindAppropriateQueue(IExecutableWorkItem workItemToExecute)
		{
			TimeSpan ts = workItemToExecute.ExecutionTime.Subtract(CurrentDateTime);
			if (ts.Milliseconds > LargeDelayQueueCutoffMS)
				return TotalThreads - 1;
			else if (ts.Milliseconds > ModerateDelayQueueCutoffMS)
				return TotalThreads - 2;
			else
				return Interlocked.Increment(ref counter) % (TotalThreads - 2);
		}

		private void Initialize()
		{
			_allWorkItemQueues = new ThreadSafeQueue<IExecutableWorkItem>[TotalThreads];
			_workItemQueueHandlers = new WorkItemQueueHandler[TotalThreads];
			_exceptions = new List<Exception>();
			_currentDateTime = DateTime.Now;
		}

		private DateTime UpdateDateTime()
		{
			_currentDateTime = DateTime.Now;// Globals.TimeFlow.UpdateTime();
			return _currentDateTime;
		}

		private int UpdateWorkItemSchedule(int queueNumber)
		{
			int workItemsMoved = 0;
			IExecutableWorkItem item;

			// Store items that need to go back into this 'slow-track' queue
			List<IExecutableWorkItem> itemsBackIntoOriginalQueue = new List<IExecutableWorkItem>();
			List<IExecutableWorkItem> workItems = new List<IExecutableWorkItem>(16);

			while (_allWorkItemQueues[queueNumber].DequeueMultiple(workItems, 10) > 0 && !WantExit)
			{
				for (int n = 0; n < workItems.Count; n++)
				{
					item = workItems[n];

					// Determine the appropriate work item queue for this item, based on its scheduled execution time
					int appropriateQueue = FindAppropriateQueue(item);

					// Check if this item needs to be moved into a different queue
					if (queueNumber != appropriateQueue)
					{
						_allWorkItemQueues[appropriateQueue].Enqueue(item);
						workItemsMoved++;
					}
					else
					{
						// We will need to put the item back into the original queue
						itemsBackIntoOriginalQueue.Add(item);
					}
				}

				workItems.Clear();
			}

			// Return the work items that did not need to be moved to the slow-track queue
			_allWorkItemQueues[queueNumber].EnqueueMultiple(itemsBackIntoOriginalQueue);

			return workItemsMoved;
		}		
	}
}